home *** CD-ROM | disk | FTP | other *** search
/ Chip 2007 January, February, March & April / Chip-Cover-CD-2007-02.iso / Pakiet bezpieczenstwa / mini Pentoo LiveCD 2006.1 / mpentoo-2006.1.iso / livecd.squashfs / usr / bin / eclean < prev    next >
Text File  |  2006-05-08  |  30KB  |  802 lines

  1. #!/usr/bin/env python
  2. # Copyright 2003-2005 Gentoo Foundation
  3. # Distributed under the terms of the GNU General Public License v2
  4. # $Header: $
  5.  
  6.  
  7. ###############################################################################
  8. # Meta:
  9. __author__ = "Thomas de Grenier de Latour (tgl)"
  10. __email__ = "degrenier@easyconnect.fr"
  11. __version__ = "0.4.1"
  12. __productname__ = "eclean"
  13. __description__ = "A cleaning tool for Gentoo distfiles and binaries."
  14.  
  15.  
  16. ###############################################################################
  17. # Python imports:
  18. import sys
  19. import os, stat
  20. import string, re
  21. import time
  22. import getopt
  23. import fpformat
  24. import signal
  25. sys.path.insert(0,'/usr/lib/portage/pym')
  26. import portage
  27. from output import *
  28.  
  29. listdir = portage.listdir
  30.  
  31. ###############################################################################
  32. # Misc. shortcuts to some portage stuff:
  33. port_settings = portage.settings
  34. distdir = port_settings["DISTDIR"]
  35. pkgdir = port_settings["PKGDIR"]
  36.  
  37.  
  38. ###############################################################################
  39. # printVersion:
  40. def printVersion():
  41.     print "%s (version %s) - %s" \
  42.             % (__productname__, __version__, __description__)
  43.     print "Author: %s <%s>" % (__author__,__email__)
  44.     print "Copyright 2003-2005 Gentoo Foundation"
  45.     print "Distributed under the terms of the GNU General Public License v2"
  46.  
  47.  
  48. ###############################################################################
  49. # printUsage: print help message. May also print partial help to stderr if an
  50. # error from {'options','actions'} is specified.
  51. def printUsage(error=None,help=None):
  52.     out = sys.stdout
  53.     if error: out = sys.stderr
  54.     if not error in ('actions', 'global-options', \
  55.             'packages-options', 'distfiles-options', \
  56.             'merged-packages-options', 'merged-distfiles-options', \
  57.             'time', 'size'):
  58.         error = None
  59.     if not error and not help: help = 'all'
  60.     if error == 'time':
  61.         eerror("Wrong time specification")
  62.         print >>out, "Time specification should be an integer followed by a"+ \
  63.                 " single letter unit."
  64.         print >>out, "Available units are: y (years), m (months), w (weeks), "+ \
  65.                 "d (days) and h (hours)."
  66.         print >>out, "For instance: \"1y\" is \"one year\", \"2w\" is \"two"+ \
  67.                 " weeks\", etc. "
  68.         return
  69.     if error == 'size':
  70.         eerror("Wrong size specification")
  71.         print >>out, "Size specification should be an integer followed by a"+ \
  72.                 " single letter unit."
  73.         print >>out, "Available units are: G, M, K and B."
  74.         print >>out, "For instance: \"10M\" is \"ten megabytes\", \"200K\" "+ \
  75.                 "is \"two hundreds kilobytes\", etc."
  76.         return
  77.     if error in ('global-options', 'packages-options', 'distfiles-options', \
  78.             'merged-packages-options', 'merged-distfiles-options',):
  79.         eerror("Wrong option on command line.")
  80.         print >>out
  81.     elif error == 'actions':
  82.         eerror("Wrong or missing action name on command line.")
  83.         print >>out
  84.     print >>out, white("Usage:")
  85.     if error in ('actions','global-options', 'packages-options', \
  86.     'distfiles-options') or help == 'all':
  87.         print >>out, " "+turquoise(__productname__), \
  88.             yellow("[global-option] ..."), \
  89.             green("<action>"), \
  90.             yellow("[action-option] ...")
  91.     if error == 'merged-distfiles-options' or help in ('all','distfiles'):
  92.         print >>out, " "+turquoise(__productname__+'-dist'), \
  93.             yellow("[global-option, distfiles-option] ...")
  94.     if error == 'merged-packages-options' or help in ('all','packages'):
  95.         print >>out, " "+turquoise(__productname__+'-pkg'), \
  96.             yellow("[global-option, packages-option] ...")
  97.     if error in ('global-options', 'actions'):
  98.         print >>out, " "+turquoise(__productname__), \
  99.             yellow("[--help, --version]")
  100.     if help == 'all':
  101.         print >>out, " "+turquoise(__productname__+"(-dist,-pkg)"), \
  102.             yellow("[--help, --version]")
  103.     if error == 'merged-packages-options' or help == 'packages':
  104.         print >>out, " "+turquoise(__productname__+'-pkg'), \
  105.             yellow("[--help, --version]")
  106.     if error == 'merged-distfiles-options' or help == 'distfiles':
  107.         print >>out, " "+turquoise(__productname__+'-dist'), \
  108.             yellow("[--help, --version]")
  109.     print >>out
  110.     if error in ('global-options', 'merged-packages-options', \
  111.     'merged-distfiles-options') or help:
  112.         print >>out, "Available global", yellow("options")+":"
  113.         print >>out, yellow(" -C, --nocolor")+ \
  114.             "             - turn off colors on output"
  115.         print >>out, yellow(" -d, --destructive")+ \
  116.             "         - only keep the minimum for a reinstallation"
  117.         print >>out, yellow(" -e, --exclude-file=<path>")+ \
  118.             " - path to the exclusion file"
  119.         print >>out, yellow(" -i, --interactive")+ \
  120.             "         - ask confirmation before deletions"
  121.         print >>out, yellow(" -n, --package-names")+ \
  122.             "       - protect all versions (when --destructive)"
  123.         print >>out, yellow(" -p, --pretend")+ \
  124.             "             - only display what would be cleaned"
  125.         print >>out, yellow(" -q, --quiet")+ \
  126.             "               - be as quiet as possible"
  127.         print >>out, yellow(" -t, --time-limit=<time>")+ \
  128.             "   - don't delete files modified since "+yellow("<time>")
  129.         print >>out, "   "+yellow("<time>"), "is a duration: \"1y\" is"+ \
  130.                 " \"one year\", \"2w\" is \"two weeks\", etc. "
  131.         print >>out, "   "+"Units are: y (years), m (months), w (weeks), "+ \
  132.                 "d (days) and h (hours)."
  133.         print >>out, yellow(" -h, --help")+ \
  134.             "                - display the help screen"
  135.         print >>out, yellow(" -V, --version")+ \
  136.             "             - display version info"
  137.         print >>out
  138.     if error == 'actions' or help == 'all':
  139.         print >>out, "Available", green("actions")+":"
  140.         print >>out, green(" packages")+ \
  141.             "      - clean outdated binary packages from:"
  142.         print >>out, "                  ",teal(pkgdir)
  143.         print >>out, green(" distfiles")+ \
  144.             "     - clean outdated packages sources files from:"
  145.         print >>out, "                  ",teal(distdir)
  146.         print >>out
  147.     if error in ('packages-options','merged-packages-options') \
  148.     or help in ('all','packages'):
  149.         print >>out, "Available", yellow("options"),"for the", \
  150.                 green("packages"),"action:"
  151.         print >>out, yellow(" NONE  :)")
  152.         print >>out
  153.     if error in ('distfiles-options', 'merged-distfiles-options') \
  154.     or help in ('all','distfiles'):
  155.         print >>out, "Available", yellow("options"),"for the", \
  156.                 green("distfiles"),"action:"
  157.         print >>out, yellow(" -f, --fetch-restricted")+ \
  158.             "   - protect fetch-restricted files (when --destructive)"
  159.         print >>out, yellow(" -s, --size-limit=<size>")+ \
  160.             "  - don't delete disfiles bigger than "+yellow("<size>")
  161.         print >>out, "   "+yellow("<size>"), "is a size specification: "+ \
  162.                 "\"10M\" is \"ten megabytes\", \"200K\" is"
  163.         print >>out, "   "+"\"two hundreds kilobytes\", etc.  Units are: "+ \
  164.                 "G, M, K and B."
  165.         print >>out
  166.     print >>out, "More detailed instruction can be found in", \
  167.             turquoise("`man %s`" % __productname__)
  168.  
  169.  
  170. ###############################################################################
  171. # einfo: display an info message depending on a color mode
  172. def einfo(message="", nocolor=False):
  173.     if not nocolor: prefix = " "+green('*')
  174.     else: prefix = ">>>"
  175.     print prefix,message
  176.  
  177.  
  178. ###############################################################################
  179. # eerror: display an error depending on a color mode
  180. def eerror(message="", nocolor=False):
  181.     if not nocolor: prefix = " "+red('*')
  182.     else: prefix = "!!!"
  183.     print >>sys.stderr,prefix,message
  184.  
  185.  
  186. ###############################################################################
  187. # eprompt: display a user question depending on a color mode.
  188. def eprompt(message, nocolor=False):
  189.     if not nocolor: prefix = " "+red('>')+" "
  190.     else: prefix = "??? "
  191.     sys.stdout.write(prefix+message)
  192.     sys.stdout.flush()
  193.  
  194.  
  195. ###############################################################################
  196. # prettySize: integer -> byte/kilo/mega/giga converter. Optionnally justify the
  197. # result. Output is a string.
  198. def prettySize(size,justify=False):
  199.     units = [" G"," M"," K"," B"]
  200.     approx = 0
  201.     while len(units) and size >= 1000:
  202.         approx = 1
  203.         size = size / 1024.
  204.         units.pop()
  205.     sizestr = fpformat.fix(size,approx)+units[-1]
  206.     if justify: 
  207.         sizestr = " " + blue("[ ") + " "*(7-len(sizestr)) \
  208.                   + green(sizestr) + blue(" ]")
  209.     return sizestr
  210.  
  211.  
  212. ###############################################################################
  213. # yesNoAllPrompt: print a prompt until user answer in yes/no/all. Return a 
  214. # boolean for answer, and also may affect the 'accept_all' option.
  215. # Note: i gave up with getch-like functions, to much bugs in case of escape
  216. # sequences. Back to raw_input.
  217. def yesNoAllPrompt(myoptions,message="Do you want to proceed?"):
  218.     user_string="xxx"
  219.     while not user_string.lower() in ["","y","n","a","yes","no","all"]:
  220.         eprompt(message+" [Y/n/a]: ", myoptions['nocolor'])
  221.         user_string = raw_input()
  222.     if user_string.lower() in ["a","all"]:
  223.         myoptions['accept_all'] = True
  224.     myanswer = user_string.lower() in ["","y","a","yes","all"]
  225.     return myanswer
  226.  
  227.  
  228. ###############################################################################
  229. # ParseArgsException: for parseArgs() -> main() communication
  230. class ParseArgsException(Exception):
  231.     def __init__(self, value):
  232.         self.value = value
  233.     def __str__(self):
  234.         return repr(self.value)
  235.  
  236.  
  237. ###############################################################################
  238. # parseSize: convert a file size "Xu" ("X" is an integer, and "u" in [G,M,K,B])
  239. # into an integer (file size in Bytes). Raises ParseArgsException('size') in
  240. # case of failure.
  241. def parseSize(size):
  242.     myunits = { \
  243.         'G': (1024**3), \
  244.         'M': (1024**2), \
  245.         'K': 1024, \
  246.         'B': 1 \
  247.     }
  248.     try:
  249.         mymatch = re.match(r"^(?P<value>\d+)(?P<unit>[GMKBgmkb])?$",size)
  250.         mysize = int(mymatch.group('value'))
  251.         if mymatch.group('unit'):
  252.             mysize *= myunits[string.capitalize(mymatch.group('unit'))]
  253.     except:
  254.         raise ParseArgsException('size')
  255.     return mysize
  256.  
  257.  
  258. ###############################################################################
  259. # parseTime: convert a duration "Xu" ("X" is an int, and "u" a time unit in 
  260. # [Y,M,W,D,H]) into an integer which is a past EPOCH date. 
  261. # Raises ParseArgsException('time') in case of failure.
  262. # (yep, big approximations inside... who cares?)
  263. def parseTime(timespec):
  264.     myunits = {'H' : (60 * 60)}
  265.     myunits['D'] = myunits['H'] * 24
  266.     myunits['W'] = myunits['D'] * 7
  267.     myunits['M'] = myunits['D'] * 30
  268.     myunits['Y'] = myunits['D'] * 365
  269.     try:
  270.         # parse the time specification
  271.         mymatch = re.match(r"^(?P<value>\d+)(?P<unit>[YMWDHymwdh])?$",timespec)
  272.         myvalue = int(mymatch.group('value'))
  273.         if not mymatch.group('unit'): myunit = 'D'
  274.         else: myunit = string.capitalize(mymatch.group('unit'))
  275.     except: raise ParseArgsException('time')
  276.     # calculate the limit EPOCH date
  277.     mytime = time.time() - (myvalue * myunits[myunit])
  278.     return mytime
  279.  
  280.  
  281. ###############################################################################
  282. # parseCmdLine: parse the command line arguments. Raise exceptions on errors or
  283. # non-action modes (help/version). Returns an action, and affect the options
  284. # dict.
  285. def parseArgs(myoptions={}):
  286.  
  287.     # local function for interpreting command line options 
  288.     # and setting myoptions accordingly
  289.     def optionSwitch(myoption,opts,action=None):
  290.         return_code = True
  291.         for o, a in opts:
  292.             if o in ("-h", "--help"):
  293.                 if action: raise ParseArgsException('help-'+action)
  294.                 else: raise ParseArgsException('help')
  295.             elif o in ("-V", "--version"):
  296.                 raise ParseArgsException('version')
  297.             elif o in ("-C", "--nocolor"):
  298.                 myoptions['nocolor'] = True
  299.                 nocolor()
  300.             elif o in ("-d", "--destructive"):
  301.                 myoptions['destructive'] = True
  302.             elif o in ("-i", "--interactive") and not myoptions['pretend']:
  303.                 myoptions['interactive'] = True
  304.             elif o in ("-p", "--pretend"):
  305.                 myoptions['pretend'] = True
  306.                 myoptions['interactive'] = False
  307.             elif o in ("-q", "--quiet"):
  308.                 myoptions['quiet'] = True
  309.             elif o in ("-t", "--time-limit"):
  310.                 myoptions['time-limit'] = parseTime(a)
  311.             elif o in ("-e", "--exclude-file"):
  312.                 myoptions['exclude-file'] = a
  313.             elif o in ("-n", "--package-names"):
  314.                 myoptions['package-names'] = True
  315.             elif o in ("-f", "--fetch-restricted"):
  316.                 myoptions['fetch-restricted'] = True
  317.             elif o in ("-s", "--size-limit"):
  318.                 myoptions['size-limit'] = parseSize(a)
  319.             else: return_code = False
  320.         # sanity check of --destructive only options:
  321.         for myopt in ('fetch-restricted', 'package-names'):
  322.             if (not myoptions['destructive']) and myoptions[myopt]:
  323.                 if not myoptions['quiet']:    
  324.                     eerror("--%s only makes sense in --destructive mode." \
  325.                             % myopt, myoptions['nocolor'])
  326.                 myoptions[myopt] = False
  327.         return return_code
  328.  
  329.     # here are the different allowed command line options (getopt args)
  330.     getopt_options = {'short':{}, 'long':{}}
  331.     getopt_options['short']['global'] = "Cdipqe::t::nhV"
  332.     getopt_options['long']['global'] = ["nocolor", "destructive", \
  333.             "interactive", "pretend", "quiet", "exclude-file", "time-limit", \
  334.             "package-names", "help", "version"]
  335.     getopt_options['short']['distfiles'] = "fs::"
  336.     getopt_options['long']['distfiles'] = ["fetch-restricted", "size-limit"]
  337.     getopt_options['short']['packages'] = ""
  338.     getopt_options['long']['packages'] = [""]
  339.     # set default options, except 'nocolor', which is set in main()
  340.     myoptions['interactive'] = False
  341.     myoptions['pretend'] = False
  342.     myoptions['quiet'] = False
  343.     myoptions['accept_all'] = False
  344.     myoptions['destructive'] = False
  345.     myoptions['time-limit'] = 0
  346.     myoptions['package-names'] = False
  347.     myoptions['fetch-restricted'] = False
  348.     myoptions['size-limit'] = 0
  349.     # if called by a well-named symlink, set the acction accordingly:
  350.     myaction = None
  351.     if os.path.basename(sys.argv[0]) in \
  352.             (__productname__+'-pkg', __productname__+'-packages'):
  353.         myaction = 'packages'
  354.     elif os.path.basename(sys.argv[0]) in \
  355.             (__productname__+'-dist', __productname__+'-distfiles'):
  356.         myaction = 'distfiles'
  357.     # prepare for the first getopt
  358.     if myaction:
  359.         short_opts = getopt_options['short']['global'] \
  360.                 + getopt_options['short'][myaction]
  361.         long_opts = getopt_options['long']['global'] \
  362.                 + getopt_options['long'][myaction]
  363.         opts_mode = 'merged-'+myaction
  364.     else:
  365.         short_opts = getopt_options['short']['global']
  366.         long_opts = getopt_options['long']['global']
  367.         opts_mode = 'global'
  368.     # apply getopts to command line, show partial help on failure
  369.     try: opts, args = getopt.getopt(sys.argv[1:], short_opts, long_opts)
  370.     except: raise ParseArgsException(opts_mode+'-options')
  371.     # set myoptions accordingly
  372.     optionSwitch(myoptions,opts,action=myaction)
  373.     # if action was already set, there should be no more args
  374.     if myaction and len(args): raise ParseArgsException(opts_mode+'-options')
  375.     # if action was set, there is nothing left to do
  376.     if myaction: return myaction
  377.     # So, we are in "eclean --foo action --bar" mode. Parse remaining args...
  378.     # Only two actions are allowed: 'packages' and 'distfiles'.
  379.     if not len(args) or not args[0] in ('packages','distfiles'):
  380.         raise ParseArgsException('actions')
  381.     myaction = args.pop(0)
  382.     # parse the action specific options
  383.     try: opts, args = getopt.getopt(args, \
  384.             getopt_options['short'][myaction], \
  385.             getopt_options['long'][myaction])
  386.     except: raise ParseArgsException(myaction+'-options')
  387.     # set myoptions again, for action-specific options
  388.     optionSwitch(myoptions,opts,action=myaction)
  389.     # any remaning args? Then die!
  390.     if len(args): raise ParseArgsException(myaction+'-options')
  391.     # returns the action. Options dictionary is modified by side-effect.
  392.     return myaction
  393.  
  394. ###############################################################################
  395. # isValidCP: check wether a string is a valid cat/pkg-name
  396. # This is for 2.0.51 vs. CVS HEAD compatibility, i've not found any function
  397. # for that which would exists in both. Weird...
  398. def isValidCP(cp):
  399.     if not '/' in cp: return False
  400.     try: portage.cpv_getkey(cp+"-0")
  401.     except: return False
  402.     else: return True
  403.  
  404.  
  405. ###############################################################################
  406. # ParseExcludeFileException: for parseExcludeFile() -> main() communication
  407. class ParseExcludeFileException(Exception):
  408.     def __init__(self, value):
  409.         self.value = value
  410.     def __str__(self):
  411.         return repr(self.value)
  412.  
  413.  
  414. ###############################################################################
  415. # parseExcludeFile: parses an exclusion file, returns an exclusion dictionnary
  416. # Raises ParseExcludeFileException in case of fatal error.
  417. def parseExcludeFile(filepath):
  418.     excl_dict = { \
  419.             'categories':{}, \
  420.             'packages':{}, \
  421.             'anti-packages':{}, \
  422.             'garbage':{} }
  423.     try: file = open(filepath,"r")
  424.     except IOError:
  425.         raise ParseExcludeFileException("Could not open exclusion file.")
  426.     filecontents = file.readlines()
  427.     file.close()
  428.     cat_re = re.compile('^(?P<cat>[a-zA-Z0-9]+-[a-zA-Z0-9]+)(/\*)?$')
  429.     cp_re = re.compile('^(?P<cp>[-a-zA-Z0-9_]+/[-a-zA-Z0-9_]+)$')    
  430.     for line in filecontents:
  431.         line = line.strip()
  432.         if not len(line): continue
  433.         if line[0] == '#': continue
  434.         try: mycat = cat_re.match(line).group('cat')
  435.         except: pass
  436.         else: 
  437.             if not mycat in portage.settings.categories:
  438.                 raise ParseExcludeFileException("Invalid category: "+mycat)
  439.             excl_dict['categories'][mycat] = None
  440.             continue
  441.         dict_key = 'packages'
  442.         if line[0] == '!':
  443.             dict_key = 'anti-packages'
  444.             line = line[1:]
  445.         try:
  446.             mycp = cp_re.match(line).group('cp')
  447.             if isValidCP(mycp):
  448.                 excl_dict[dict_key][mycp] = None
  449.                 continue
  450.             else: raise ParseExcludeFileException("Invalid cat/pkg: "+mycp)
  451.         except: pass
  452.         #raise ParseExcludeFileException("Invalid line: "+line)
  453.         try:
  454.             excl_dict['garbage'][line] = re.compile(line)
  455.         except:
  456.             try:
  457.                 excl_dict['garbage'][line] = re.compile(re.escape(line))
  458.             except:
  459.                 raise ParseExcludeFileException("Invalid file name/regular expression: "+line)
  460.     return excl_dict
  461.  
  462.  
  463. ###############################################################################
  464. # exclDictExpand: returns a dictionary of all CP from porttree which match
  465. # the exclusion dictionary
  466. def exclDictExpand(excl_dict):
  467.     mydict = {}
  468.     if 'categories' in excl_dict:
  469.         # XXX: i smell an access to something which is really out of API...
  470.         for mytree in portage.portdb.porttrees:
  471.             for mycat in excl_dict['categories']:
  472.                 for mypkg in listdir(os.path.join(mytree,mycat),ignorecvs=1):
  473.                     mydict[mycat+'/'+mypkg] = None
  474.     if 'packages' in excl_dict:
  475.         for mycp in excl_dict['packages']:
  476.             mydict[mycp] = None
  477.     if 'anti-packages' in excl_dict:
  478.         for mycp in excl_dict['anti-packages']:
  479.             if mycp in mydict:
  480.                 del mydict[mycp]
  481.     return mydict
  482.  
  483.  
  484. ###############################################################################
  485. # exclDictMatch: checks whether a CP matches the exclusion rules
  486. def exclDictMatch(excl_dict,pkg):
  487.     if 'anti-packages' in excl_dict \
  488.        and pkg in excl_dict['anti-packages']:
  489.         return False
  490.     if 'packages' in excl_dict \
  491.        and pkg in excl_dict['packages']:
  492.         return True
  493.     mycat = pkg.split('/')[0]
  494.     if 'categories' in excl_dict \
  495.        and mycat in excl_dict['categories']:
  496.         return True
  497.     return False
  498.  
  499.  
  500. ###############################################################################
  501. # findDistfiles: find all obsolete distfiles.
  502. # XXX: what about cvs ebuilds? i should install some to see where it goes...
  503. def findDistfiles( \
  504.         exclude_dict={}, \
  505.         destructive=False,\
  506.         fetch_restricted=False, \
  507.         package_names=False, \
  508.         time_limit=0, \
  509.         size_limit=0):
  510.     # this regexp extracts files names from SRC_URI. It is not very precise,
  511.     # but we don't care (may return empty strings, etc.), since it is fast.
  512.     file_regexp = re.compile('([a-zA-Z0-9_,\.\-\+]*)[\s\)]')
  513.     clean_dict = {}
  514.     keep = []
  515.     pkg_dict = {}
  516.  
  517.     # create a big CPV->SRC_URI dict of packages whose distfiles should be kept
  518.     if (not destructive) or fetch_restricted:
  519.         # list all CPV from portree (yeah, that takes time...)
  520.         for package in portage.portdb.cp_all():
  521.             for my_cpv in portage.portdb.cp_list(package):
  522.                 # get SRC_URI and RESTRICT from aux_get
  523.                 try: (src_uri,restrict) = \
  524.                     portage.portdb.aux_get(my_cpv,["SRC_URI","RESTRICT"])
  525.                 except KeyError: continue
  526.                 # keep either all or fetch-restricted only
  527.                 if (not destructive) or ('fetch' in restrict):
  528.                     pkg_dict[my_cpv] = src_uri
  529.     if destructive:
  530.         if not package_names:
  531.             # list all CPV from vartree
  532.             pkg_list = portage.db[portage.root]["vartree"].dbapi.cpv_all()
  533.         else:
  534.             # list all CPV from portree for CP in vartree
  535.             pkg_list = []
  536.             for package in portage.db[portage.root]["vartree"].dbapi.cp_all():
  537.                 pkg_list += portage.portdb.cp_list(package)
  538.         for my_cp in exclDictExpand(exclude_dict):
  539.             # add packages from the exclude file
  540.             pkg_list += portage.portdb.cp_list(my_cp)
  541.         for my_cpv in pkg_list:
  542.             # skip non-existing CPV (avoids ugly aux_get messages)
  543.             if not portage.portdb.cpv_exists(my_cpv): continue
  544.             # get SRC_URI from aux_get
  545.             try: pkg_dict[my_cpv] = \
  546.                     portage.portdb.aux_get(my_cpv,["SRC_URI"])[0]
  547.             except KeyError: continue
  548.         del pkg_list
  549.  
  550.     # create a dictionary of files which should be deleted 
  551.     for file in os.listdir(distdir):
  552.         filepath = os.path.join(distdir, file)
  553.         try: file_stat = os.stat(filepath)
  554.         except: continue
  555.         if not stat.S_ISREG(file_stat[stat.ST_MODE]): continue
  556.         if size_limit and (file_stat[stat.ST_SIZE] >= size_limit):
  557.             continue
  558.         if time_limit and (file_stat[stat.ST_MTIME] >= time_limit):
  559.             continue
  560.         if 'garbage' in exclude_dict:
  561.             # Try to match file name directly
  562.             if file in exclude_dict['garbage']:
  563.                 file_match = True
  564.             # See if file matches via regular expression matching
  565.             else:
  566.                 file_match = False
  567.                 for file_entry in exclude_dict['garbage']:
  568.                     if exclude_dict['garbage'][file_entry].match(file):
  569.                         file_match = True
  570.                         break
  571.  
  572.             if file_match:
  573.                 continue
  574.         # this is a candidate for cleaning
  575.         clean_dict[file]=[filepath]
  576.     # remove files owned by some protected packages
  577.     for my_cpv in pkg_dict:
  578.         for file in file_regexp.findall(pkg_dict[my_cpv]+"\n"):
  579.             if file in clean_dict:
  580.                 del clean_dict[file]
  581.         # no need to waste IO time if there is nothing left to clean
  582.         if not len(clean_dict): return clean_dict
  583.     return clean_dict
  584.  
  585.  
  586. ###############################################################################
  587. # findPackages: find all obsolete binary packages.
  588. # XXX: packages are found only by symlinks. Maybe i should also return .tbz2
  589. #      files from All/ that have no corresponding symlinks.
  590. def findPackages( \
  591.         exclude_dict={}, \
  592.         destructive=False, \
  593.         time_limit=0, \
  594.         package_names=False):
  595.     clean_dict = {}
  596.     # create a full package dictionnary
  597.     for root, dirs, files in os.walk(pkgdir):
  598.         if root[-3:] == 'All': continue
  599.         for file in files:
  600.             if not file[-5:] == ".tbz2":
  601.                 # ignore non-tbz2 files
  602.                 continue
  603.             path = os.path.join(root, file)
  604.             category = os.path.split(root)[-1]
  605.             cpv = category+"/"+file[:-5]
  606.             mystat = os.lstat(path)
  607.             if time_limit and (mystat[stat.ST_MTIME] >= time_limit):
  608.                 # time-limit exclusion
  609.                 continue
  610.             # dict is cpv->[files] (2 files in general, because of symlink)
  611.             clean_dict[cpv] = [path]
  612.             #if os.path.islink(path):
  613.             if stat.S_ISLNK(mystat[stat.ST_MODE]):
  614.                 clean_dict[cpv].append(os.path.realpath(path))
  615.     # keep only obsolete ones
  616.     if destructive:
  617.         mydbapi = portage.db[portage.root]["vartree"].dbapi
  618.         if package_names: cp_all = dict.fromkeys(mydbapi.cp_all())
  619.         else: cp_all = {}
  620.     else:
  621.         mydbapi = portage.db[portage.root]["porttree"].dbapi
  622.         cp_all = {}
  623.     for mycpv in clean_dict.keys():
  624.         if exclDictMatch(exclude_dict,portage.cpv_getkey(mycpv)):
  625.             # exclusion because of the exclude file
  626.             del clean_dict[mycpv]
  627.             continue
  628.         if mydbapi.cpv_exists(mycpv):
  629.             # exclusion because pkg still exists (in porttree or vartree)
  630.             del clean_dict[mycpv]
  631.             continue
  632.         if portage.cpv_getkey(mycpv) in cp_all:
  633.             # exlusion because of --package-names 
  634.             del clean_dict[mycpv]
  635.  
  636.     return clean_dict
  637.  
  638.  
  639. ###############################################################################
  640. # doCleanup: takes a dictionnary {'display name':[list of files]}. Calculate
  641. # size of each entry for display, prompt user if needed, delete files if needed
  642. # and return the total size of files that [have been / would be] deleted.
  643. def doCleanup(clean_dict,action,myoptions):
  644.     # define vocabulary of this action
  645.     if action == 'distfiles': file_type = 'file'
  646.     else: file_type = 'binary package'
  647.     # sorting helps reading
  648.     clean_keys = clean_dict.keys()
  649.     clean_keys.sort()
  650.     clean_size = 0
  651.     # clean all entries one by one
  652.     for mykey in clean_keys:
  653.         key_size = 0
  654.         for file in clean_dict[mykey]:
  655.             # get total size for an entry (may be several files, and
  656.             # symlinks count zero)
  657.             if os.path.islink(file): continue
  658.             try: key_size += os.path.getsize(file)
  659.             except: eerror("Could not read size of "+file, \
  660.                            myoptions['nocolor'])
  661.         if not myoptions['quiet']:
  662.             # pretty print mode
  663.             print prettySize(key_size,True),teal(mykey)
  664.         elif myoptions['pretend'] or myoptions['interactive']:
  665.             # file list mode
  666.             for file in clean_dict[mykey]: print file
  667.         #else: actually delete stuff, but don't print anything
  668.         if myoptions['pretend']: clean_size += key_size
  669.         elif not myoptions['interactive'] \
  670.              or myoptions['accept_all'] \
  671.              or yesNoAllPrompt(myoptions, \
  672.                                "Do you want to delete this " \
  673.                        + file_type+"?"):
  674.             # non-interactive mode or positive answer. 
  675.             # For each file,...
  676.             for file in clean_dict[mykey]:
  677.                 # ...get its size...
  678.                 filesize = 0
  679.                 if not os.path.exists(file): continue
  680.                 if not os.path.islink(file):
  681.                     try: filesize = os.path.getsize(file)
  682.                     except: eerror("Could not read size of "\
  683.                                    +file, myoptions['nocolor'])
  684.                 # ...and try to delete it.
  685.                 try: os.unlink(file)
  686.                 except: eerror("Could not delete "+file, \
  687.                                myoptions['nocolor'])
  688.                 # only count size if successfully deleted
  689.                 else: clean_size += filesize
  690.     # return total size of deleted or to delete files
  691.     return clean_size
  692.  
  693.  
  694. ###############################################################################
  695. # doAction: execute one action, ie display a few message, call the right find*
  696. # function, and then call doCleanup with its result.
  697. def doAction(action,myoptions,exclude_dict={}):
  698.     # define vocabulary for the output
  699.     if action == 'packages': files_type = "binary packages"
  700.     else: files_type = "distfiles"
  701.     # find files to delete, depending on the action
  702.     if not myoptions['quiet']:
  703.         einfo("Building file list for "+action+" cleaning...", \
  704.               myoptions['nocolor'])
  705.     if action == 'packages':
  706.         clean_dict = findPackages( \
  707.             exclude_dict=exclude_dict, \
  708.             destructive=myoptions['destructive'], \
  709.             package_names=myoptions['package-names'], \
  710.             time_limit=myoptions['time-limit'])
  711.     else: 
  712.         clean_dict = findDistfiles( \
  713.             exclude_dict=exclude_dict, \
  714.             destructive=myoptions['destructive'], \
  715.             fetch_restricted=myoptions['fetch-restricted'], \
  716.             package_names=myoptions['package-names'], \
  717.             time_limit=myoptions['time-limit'], \
  718.             size_limit=myoptions['size-limit'])
  719.     # actually clean files if something was found
  720.     if len(clean_dict.keys()):
  721.         # verbose pretend message
  722.         if myoptions['pretend'] and not myoptions['quiet']:
  723.             einfo("Here are "+files_type+" that would be deleted:", \
  724.                   myoptions['nocolor'])
  725.         # verbose non-pretend message
  726.         elif not myoptions['quiet']:
  727.             einfo("Cleaning "+files_type+"...",myoptions['nocolor'])
  728.         # do the cleanup, and get size of deleted files
  729.         clean_size = doCleanup(clean_dict,action,myoptions)
  730.         # vocabulary for final message
  731.         if myoptions['pretend']: verb = "would be"
  732.         else: verb = "has been"
  733.         # display freed space
  734.         if not myoptions['quiet']:
  735.             einfo("Total space that "+verb+" freed in " \
  736.                   + action + " directory: " \
  737.                   + red(prettySize(clean_size)), \
  738.                   myoptions['nocolor'])
  739.     # nothing was found, return
  740.     elif not myoptions['quiet']:
  741.         einfo("Your "+action+" directory was already clean.", \
  742.               myoptions['nocolor'])
  743.  
  744.  
  745. ###############################################################################
  746. # main: parse command line and execute all actions
  747. def main():
  748.     # set default options
  749.     myoptions = {}
  750.     myoptions['nocolor'] = port_settings["NOCOLOR"] in ('yes','true') \
  751.                            and sys.stdout.isatty()
  752.     if myoptions['nocolor']: nocolor()
  753.     # parse command line options and actions
  754.     try: myaction = parseArgs(myoptions)
  755.     # filter exception to know what message to display
  756.     except ParseArgsException, e:
  757.         if e.value == 'help':
  758.             printUsage(help='all')
  759.             sys.exit(0)
  760.         elif e.value[:5] == 'help-':
  761.             printUsage(help=e.value[5:])
  762.             sys.exit(0)
  763.         elif e.value == 'version':
  764.             printVersion()
  765.             sys.exit(0)
  766.         else: 
  767.             printUsage(e.value)
  768.             sys.exit(2)
  769.     # parse the exclusion file
  770.     if not 'exclude-file' in myoptions:
  771.         my_exclude_file = "/etc/%s/%s.exclude" % (__productname__ , myaction)
  772.         if os.path.isfile(my_exclude_file):
  773.             myoptions['exclude-file'] = my_exclude_file
  774.     if 'exclude-file' in myoptions:
  775.         try: exclude_dict = parseExcludeFile(myoptions['exclude-file'])
  776.         except ParseExcludeFileException, e:
  777.             eerror(e, myoptions['nocolor'])
  778.             eerror("Invalid exclusion file: %s" % myoptions['exclude-file'], \
  779.                     myoptions['nocolor'])
  780.             eerror("See format of this file in `man %s`" % __productname__, \
  781.                     myoptions['nocolor'])    
  782.             sys.exit(1)
  783.     else: exclude_dict={}
  784.     # security check for non-pretend mode
  785.     if not myoptions['pretend'] and portage.secpass != 2:
  786.         eerror("Permission denied: you must be root.", \
  787.                myoptions['nocolor'])
  788.         sys.exit(1)
  789.     # execute action
  790.     doAction(myaction, myoptions, exclude_dict=exclude_dict)
  791.  
  792.  
  793. ###############################################################################
  794. # actually call main() if launched as a script
  795. if __name__ == "__main__":
  796.     try: main()
  797.     except KeyboardInterrupt:
  798.         print "Aborted."
  799.         sys.exit(130)
  800.     sys.exit(0)
  801.  
  802.